/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.volatileengine.renderer.lwjgl;

import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import javax.vecmath.Vector2f;
import javax.vecmath.Vector3f;
import javax.vecmath.Vector4f;

import org.lwjgl.opengl.ARBBufferObject;
import org.lwjgl.opengl.ARBFragmentShader;
import org.lwjgl.opengl.ARBShaderObjects;
import org.lwjgl.opengl.ARBTextureCompression;
import org.lwjgl.opengl.ARBVertexBufferObject;
import org.lwjgl.opengl.ARBVertexShader;
import org.lwjgl.opengl.EXTTextureCompressionS3TC;
import org.lwjgl.opengl.GL11;

import com.volatileengine.VolatileEngineException;
import com.volatileengine.datatypes.AbstractArray;
import com.volatileengine.datatypes.AcceleratedArray;
import com.volatileengine.image.Mipmap;
import com.volatileengine.image.Texture;
import com.volatileengine.image.Mipmap.ImageType;
import com.volatileengine.material.shader.ShaderObject;
import com.volatileengine.material.shader.ShaderProgram;
import com.volatileengine.material.shader.ShaderType;
import com.volatileengine.material.shader.variable.FloatShaderVariable;
import com.volatileengine.material.shader.variable.IntShaderVariable;
import com.volatileengine.material.shader.variable.ShaderVariable;
import com.volatileengine.material.shader.variable.Vector2fShaderVariable;
import com.volatileengine.material.shader.variable.Vector3fShaderVariable;
import com.volatileengine.material.shader.variable.Vector4fShaderVariable;
import com.volatileengine.resources.references.Handle;

/**
 * 
 * @author Administrator
 */
class LWJGLHandleManager {

	// cleanup queues
	private ReferenceQueue<Texture> textures;
	private ReferenceQueue<Mipmap> mipmaps;
	private ReferenceQueue<AbstractArray<?>> vbos;
	private ReferenceQueue<ShaderObject> shaderObjects;
	private ReferenceQueue<ShaderProgram> shaderPrograms;
	private ReferenceQueue<ShaderVariable<?>> shaderUniforms;

	private ByteBuffer buffer;

	public LWJGLHandleManager() {
		textures = new ReferenceQueue<Texture>();
		mipmaps = new ReferenceQueue<Mipmap>();
		vbos = new ReferenceQueue<AbstractArray<?>>();
		shaderObjects = new ReferenceQueue<ShaderObject>();
		shaderPrograms = new ReferenceQueue<ShaderProgram>();
		shaderUniforms = new ReferenceQueue<ShaderVariable<?>>();

		buffer = ByteBuffer.allocateDirect(16 * Integer.SIZE).order(ByteOrder.nativeOrder());
	}

	public void create(Texture texture) {
		buffer.clear();
		GL11.glGenTextures(buffer.asIntBuffer());
		buffer.rewind();

		setHandleReflection(texture, texture.getClass(), new TextureHandle(texture, buffer.get(), textures));
	}

	public void create(Mipmap mipmap) {
		// upload the texture
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, mipmap.getParentTexture().getHandle().getIdentity());

		if (mipmap.isCompressed()) {
			ARBTextureCompression.glCompressedTexImage2DARB(GL11.GL_TEXTURE_2D, mipmap.getMipmapLevel(),
					convertImageType(mipmap.imageType()), mipmap.getWidth(), mipmap.getHeight(), 0, mipmap
							.getPixels().getBuffer());
		} else {
			GL11.glTexImage2D(GL11.GL_TEXTURE_2D, mipmap.getMipmapLevel(), GL11.GL_RGBA, mipmap.getWidth(), mipmap
					.getHeight(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, mipmap.getPixels().getBuffer());
		}

		setHandleReflection(mipmap, mipmap.getClass(), new MipmapHandle(mipmap, mipmap.getParentTexture(), mipmap
				.getParentTexture().getHandle().getIdentity(), mipmaps));
	}

	public List<ShaderVariable<?>> createUniform(ShaderProgram program) {
		int numUniforms = getObjectParameter(program.getHandle().getIdentity(),
				ARBShaderObjects.GL_OBJECT_ACTIVE_UNIFORMS_ARB);
		int maxUniformLen = getObjectParameter(program.getHandle().getIdentity(),
				ARBShaderObjects.GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB);
		ByteBuffer nameBuf = ByteBuffer.allocateDirect(maxUniformLen + 1).order(ByteOrder.nativeOrder());
		IntBuffer lengthBuf = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).asIntBuffer();
		IntBuffer sizeBuf = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).asIntBuffer();
		IntBuffer glTypeBuf = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).asIntBuffer();
		ArrayList<ShaderVariable<?>> newUniforms = new ArrayList<ShaderVariable<?>>(numUniforms);

		for (int i = 0; i < numUniforms; i++) {

			lengthBuf.put(0, 0);
			nameBuf.clear();
			ARBShaderObjects.glGetActiveUniformARB(program.getHandle().getIdentity(), i, lengthBuf, sizeBuf,
					glTypeBuf, nameBuf);

			if (lengthBuf.get(0) > 0) {
				final int location = ARBShaderObjects.glGetUniformLocationARB(program.getHandle().getIdentity(),
						nameBuf);
				nameBuf.limit(lengthBuf.get(0));
				String name = Charset.forName("US-ASCII").decode(nameBuf).toString();

				// ignore gl_ uniforms by having a location >= 0
				if (location >= 0) {
					if (name.indexOf('[') >= 0) {
						System.err.println("Non standard conforming OpenGL - reported " + name + " as uniform");
						if (name.endsWith("[0]")) {
							name = name.substring(0, name.length() - 3);
						} else {
							continue;
						}
					}

					final int arrSize = sizeBuf.get(0);
					final int type = glTypeBuf.get(0);

					if (arrSize > 1) {
						for (int idx = 0; idx < arrSize; idx++) {
							// createUniform(newUniforms, name + "[" +
							// idx + "]", location + idx, type);
						}
					} else {
						createShaderVariable(newUniforms, program, name, location, type);
					}
				}
			}
		}

		return newUniforms;
	}

	public List<ShaderVariable<?>> createAttributes(ShaderProgram program) {
		int numAttribs = getObjectParameter(program.getHandle().getIdentity(),
				ARBVertexShader.GL_OBJECT_ACTIVE_ATTRIBUTES_ARB);
		int maxAttribLen = getObjectParameter(program.getHandle().getIdentity(),
				ARBVertexShader.GL_OBJECT_ACTIVE_ATTRIBUTE_MAX_LENGTH_ARB);
		ByteBuffer nameBuf = ByteBuffer.allocateDirect(maxAttribLen + 1).order(ByteOrder.nativeOrder());
		IntBuffer lengthBuf = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).asIntBuffer();
		IntBuffer sizeBuf = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).asIntBuffer();
		IntBuffer glTypeBuf = ByteBuffer.allocateDirect(16).order(ByteOrder.nativeOrder()).asIntBuffer();
		ArrayList<ShaderVariable<?>> newAttribs = new ArrayList<ShaderVariable<?>>(numAttribs);

		for (int i = 0; i < numAttribs; i++) {

			lengthBuf.put(0, 0);
			nameBuf.clear();
			ARBVertexShader.glGetActiveAttribARB(program.getHandle().getIdentity(), i, lengthBuf, sizeBuf,
					glTypeBuf, nameBuf);

			if (lengthBuf.get(0) > 0) {
				final int location = ARBShaderObjects.glGetUniformLocationARB(program.getHandle().getIdentity(),
						nameBuf);
				nameBuf.limit(lengthBuf.get(0));
				String name = Charset.forName("US-ASCII").decode(nameBuf).toString();

				// ignore gl_ uniforms by having a location >= 0
				if (location >= 0) {
					if (name.indexOf('[') >= 0) {
						System.err.println("Non standard conforming OpenGL - reported " + name + " as uniform");
						if (name.endsWith("[0]")) {
							name = name.substring(0, name.length() - 3);
						} else {
							continue;
						}
					}

					final int arrSize = sizeBuf.get(0);
					final int type = glTypeBuf.get(0);

					if (arrSize > 1) {
						for (int idx = 0; idx < arrSize; idx++) {
							// createUniform(newUniforms, name + "[" +
							// idx + "]", location + idx, type);
						}
					} else {
						createShaderVariable(newAttribs, program, name, location, type);
					}
				}
			}
		}

		return newAttribs;
	}

	private void createShaderVariable(List<ShaderVariable<?>> list, ShaderProgram prog, String name, int id, int type) {
		switch (type) {
		case ARBShaderObjects.GL_INT:
			ShaderVariable<Integer> intV = new IntShaderVariable(name);
			// cheat a bit and reflect to the handle
			setHandleReflection(intV, intV.getClass(), new ShaderVariableHandle(intV, prog, id, shaderUniforms));
			list.add(intV);
			break;
		case ARBShaderObjects.GL_FLOAT:
			ShaderVariable<Float> floatV = new FloatShaderVariable(name);
			// cheat a bit and reflect to the handle
			setHandleReflection(floatV, floatV.getClass(),
					new ShaderVariableHandle(floatV, prog, id, shaderUniforms));
			list.add(floatV);
			break;
		case ARBShaderObjects.GL_FLOAT_VEC2_ARB:
			ShaderVariable<Vector2f> var2 = new Vector2fShaderVariable(name);
			// cheat a bit and reflect to the handle
			setHandleReflection(var2, var2.getClass(), new ShaderVariableHandle(var2, prog, id, shaderUniforms));
			list.add(var2);
			break;
		case ARBShaderObjects.GL_FLOAT_VEC3_ARB:
			ShaderVariable<Vector3f> var3 = new Vector3fShaderVariable(name);
			// cheat a bit and reflect to the handle
			setHandleReflection(var3, var3.getClass(), new ShaderVariableHandle(var3, prog, id, shaderUniforms));
			list.add(var3);
			break;
		case ARBShaderObjects.GL_FLOAT_VEC4_ARB:
			ShaderVariable<Vector4f> var4 = new Vector4fShaderVariable(name);
			// cheat a bit and reflect to the handle
			setHandleReflection(var4, var4.getClass(), new ShaderVariableHandle(var4, prog, id, shaderUniforms));
			list.add(var4);
			break;
		default:
			throw new VolatileEngineException(
					"Volatile-Engine doesn't have a native class to match type for Uniform: " + name
							+ " of type: " + type);
		}

		return;

	}

	private int convertImageType(ImageType type) {
		switch (type) {
		case DXT1:
			return EXTTextureCompressionS3TC.GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
		case DXT1A:
			return EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
		case DXT3:
			return EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
		case DXT5:
			return EXTTextureCompressionS3TC.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
		}

		// to satisfy compiler
		return -1;
	}

	public void create(AcceleratedArray<?> array) {
		buffer.clear();
		ARBBufferObject.glGenBuffersARB(buffer.asIntBuffer());
		int id = buffer.get();

		int mode = array.isElement() ? ARBVertexBufferObject.GL_ELEMENT_ARRAY_BUFFER_ARB
				: ARBVertexBufferObject.GL_ARRAY_BUFFER_ARB;
		ARBBufferObject.glBindBufferARB(mode, id);
		switch (array.getType()) {
		case Int:
			ARBBufferObject.glBufferDataARB(mode, (IntBuffer) array.getEncapsulatedArray().getBuffer().rewind(),
					ARBBufferObject.GL_STATIC_DRAW_ARB);
			break;
		case Float:
			ARBBufferObject.glBufferDataARB(mode, (FloatBuffer) array.getEncapsulatedArray().getBuffer().rewind(),
					ARBBufferObject.GL_STATIC_DRAW_ARB);
			break;
		case Byte:
			ARBBufferObject.glBufferDataARB(mode, (ByteBuffer) array.getEncapsulatedArray().getBuffer().rewind(),
					ARBBufferObject.GL_STATIC_DRAW_ARB);
			break;
		}

		setHandleReflection(array, array.getClass(), new VertexBufferObjectHandle(array, id, vbos));
	}

	public void create(ShaderObject shaderObj) {
		buffer.clear();
		IntBuffer intBuff = buffer.asIntBuffer();
		int obj = ARBShaderObjects.glCreateShaderObjectARB(convertShaderType(shaderObj.getShaderType()));

		ARBShaderObjects.glShaderSourceARB(obj, shaderObj.getShader());
		ARBShaderObjects.glCompileShaderARB(obj);
		ARBShaderObjects.glGetObjectParameterARB(obj, ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB, intBuff);
		intBuff.rewind();
		if (intBuff.get(0) != 1) {
			String log = getInfoLog(obj);
			ARBShaderObjects.glDeleteObjectARB(obj);
			throw new VolatileEngineException("Compilation Exception for  " + shaderObj + " of type: "
					+ shaderObj.getShaderType() + System.getProperty("line.separator") + log);
		}

		setHandleReflection(shaderObj, shaderObj.getClass(), new ShaderObjectHandle(shaderObj, obj, shaderObjects));
	}

	public void create(ShaderProgram shaderProg) {
		buffer.clear();
		IntBuffer intBuff = buffer.asIntBuffer();
		int id = ARBShaderObjects.glCreateProgramObjectARB();

		// attach all programs
		for (int i = 0; i < shaderProg.getShaderObjects().size(); i++) {
			ShaderObject obj = shaderProg.getShaderObjects().get(i);
			ARBShaderObjects.glAttachObjectARB(id, obj.getHandle().getIdentity());
		}

		ARBShaderObjects.glLinkProgramARB(id);
		ARBShaderObjects.glGetObjectParameterARB(id, ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB, intBuff);
		intBuff.rewind();
		if (intBuff.get(0) != 1) {
			String log = getInfoLog(id);
			ARBShaderObjects.glDeleteObjectARB(id);
			throw new VolatileEngineException("Link Exception in :" + shaderProg + " caused by: \n" + log);
		}

		setHandleReflection(shaderProg, shaderProg.getClass(),
				new ShaderProgramHandle(shaderProg, id, shaderPrograms));
	}

	private int getObjectParameter(int id, int param) {
		buffer.clear();
		IntBuffer intBuff = buffer.asIntBuffer();
		ARBShaderObjects.glGetObjectParameterARB(id, param, intBuff);
		intBuff.rewind();
		return intBuff.get(0);
	}

	private void setHandleReflection(Object obj, Class<?> clazz, Handle<?> handle) {
		if (clazz != null) {
			for (int i = 0; i < clazz.getDeclaredFields().length; i++) {
				if (Handle.class.isAssignableFrom(clazz.getDeclaredFields()[i].getType())) {
					Field f = clazz.getDeclaredFields()[i];

					f.setAccessible(true);
					try {
						f.set(obj, handle);
						return;
					} catch (IllegalArgumentException e) {
						throw new VolatileEngineException(e);
					} catch (IllegalAccessException e) {
						throw new VolatileEngineException(e);
					}
				}
			}

			// we haven't found a handle, go to the parent class
			setHandleReflection(obj, clazz.getSuperclass(), handle);
		} else {
			throw new VolatileEngineException("Failed to assign Handle to: " + obj);
		}
	}

	private int convertShaderType(ShaderType type) {
		switch (type) {
		case FRAGMENT:
			return ARBFragmentShader.GL_FRAGMENT_SHADER_ARB;
		case VERTEX:
			return ARBVertexShader.GL_VERTEX_SHADER_ARB;
		default:
			// to satisfy the compiler
			return -1;
		}
	}

	private String getInfoLog(int obj) {
		buffer.clear();
		ARBShaderObjects.glGetObjectParameterARB(obj, ARBShaderObjects.GL_OBJECT_INFO_LOG_LENGTH_ARB, buffer
				.asIntBuffer());
		int length = buffer.asIntBuffer().get(0);
		buffer.clear();
		buffer.asIntBuffer().put(length);
		ByteBuffer logBuff = ByteBuffer.allocateDirect(length).order(ByteOrder.nativeOrder());
		ARBShaderObjects.glGetInfoLogARB(obj, buffer.asIntBuffer(), logBuff);
		return Charset.forName("US-ASCII").decode(logBuff).toString();
	}

	private class TextureHandle extends Handle<Texture> {

		public TextureHandle(Texture resource, int id, ReferenceQueue<Texture> queue) {
			super(resource, id, queue);
		}

		@Override
		public HandleType getHandleType() {
			return HandleType.TEXTURE;
		}
	}

	private class MipmapHandle extends Handle<Mipmap> {

		private Texture texture;

		public MipmapHandle(Mipmap resource, Texture texture, int id, ReferenceQueue<Mipmap> queue) {
			super(resource, id, queue);
			this.texture = texture;
		}

		/**
		 * @return the texture
		 */
		public Texture getTexture() {
			return texture;
		}

		@Override
		public HandleType getHandleType() {
			return HandleType.TEXTURE;
		}
	}

	private class VertexBufferObjectHandle extends Handle<AbstractArray<?>> {

		public VertexBufferObjectHandle(AbstractArray<?> resource, int id, ReferenceQueue<AbstractArray<?>> queue) {
			super(resource, id, queue);
		}

		@Override
		public HandleType getHandleType() {
			return HandleType.VERTEX_BUFFER_OBJECT;
		}
	}

	private class ShaderObjectHandle extends Handle<ShaderObject> {

		public ShaderObjectHandle(ShaderObject resource, int id, ReferenceQueue<ShaderObject> queue) {
			super(resource, id, queue);
		}

		@Override
		public HandleType getHandleType() {
			return HandleType.SHADER;
		}
	}

	private class ShaderProgramHandle extends Handle<ShaderProgram> {

		public ShaderProgramHandle(ShaderProgram resource, int id, ReferenceQueue<ShaderProgram> queue) {
			super(resource, id, queue);
		}

		@Override
		public HandleType getHandleType() {
			return HandleType.SHADER;
		}
	}

	private class ShaderVariableHandle extends Handle<ShaderVariable<?>> {

		private ShaderProgram program;
		private int hashcode;

		public ShaderVariableHandle(ShaderVariable<?> resource, ShaderProgram program, int id,
				ReferenceQueue<ShaderVariable<?>> queue) {
			super(resource, id, queue);
			this.program = program;
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			int objHash = obj.hashCode();
			if (objHash == hashcode) {
				return true;
			} else {
				hashcode = objHash;
				return false;
			}
		}

		/**
		 * @return the program
		 */
		public ShaderProgram getProgram() {
			return program;
		}

		@Override
		public HandleType getHandleType() {
			return HandleType.SHADER_UNIFORM;
		}

	}

}
